Skip to content

Conversation

@tanishiking
Copy link
Member

@tanishiking tanishiking commented Dec 6, 2025

Previously, method type parameters were always renamed if they had the same name as any class type parameter, regardless of whether the class type parameter was actually used in the method signature.

As a result, we rename methods's type params for unnecessary names like: #24671 (Note that renaming actually doesn't cause binary incompatibility, and this change is just for make generic signature a bit clear and silence false-positive MIMA reporting).

For example, in an example below, the Scala compiler rename the generic signature of bar to something like bar[T1](x: T1): T1 because T is used by Foo[T]. However, this is unnessary rename because none of T in method signature refer the T of Foo[T].

class Foo[T]:
  def bar[T](x: T): T = ???

This commit makes the renaming conditional:
Method type parameters are only renamed when

  • (1) A class type parameter is referenced in the method signature, and
  • (2) That class type parameter is shadowed by a method type parameter

Previously, method type parameters were always renamed if they had the same name as any class type parameter, regardless of whether the class type parameter was actually used in the method signature.

As a result, we rename methods's type params for unnecessary names like: scala#24671
(Note that renaming actually doesn't cause binary incompatibility, and this change is just for make generic signature a bit clear and silence false-positive MIMA reporting).

For example, in an example below, the Scala compiler rename the generic signature of `bar` to something like `bar[T1](x: T1): T1` because `T` is used by `Foo[T]`. However, this is unnessary rename because none of T in method signature refer the `T` of `Foo[T]`.

```scala
class Foo[T]:
  def bar[T](x: T): T = ???
```

This commit makes the renaming conditional:
Method type parameters are only renamed when
- (1) A class type parameter is referenced in the method signature, and
- (2) That class type parameter is shadowed by a method type parameter
Comment on lines 27 to 30
// copy$default$1 still have `<T1> T Bar.copy$default$1` rather than `<T> T Bar.copy$default$1`
// as reported in https://github.com/scala/scala3/issues/24671
// The type parameter rename occurs because the return type T refers the enclosing class's type param T.
// printMethodSig(barMethods, "copy$default$1")
Copy link
Member Author

@tanishiking tanishiking Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still MiMa won't be happy with <T1> T Bar.copy$default$1 rather than <T> T Bar.copy$default$1 (but the original signature was wrong, the return type T is shadowed by method type parameter.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! I didn't pay attention. A small reproducer is

abstract class C[T] {
  def x: T
  def m[T] = x
}

We have that wrong in Scala 2, I created scala/bug#13144.

Copy link
Member Author

@tanishiking tanishiking Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! that's a good reproducer :)
It seems like renaming to <T1:Ljava/lang/Object;>()TT; is right thing to do 👍

Copy link
Member

@lrytz lrytz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

false-positive MIMA reporting

nitpicking.. it's not really a false positive report by mima, but mima can be configured to check changes in the classfile (generic signatures) that go beyond binary compatibility.

Comment on lines 27 to 30
// copy$default$1 still have `<T1> T Bar.copy$default$1` rather than `<T> T Bar.copy$default$1`
// as reported in https://github.com/scala/scala3/issues/24671
// The type parameter rename occurs because the return type T refers the enclosing class's type param T.
// printMethodSig(barMethods, "copy$default$1")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! I didn't pay attention. A small reproducer is

abstract class C[T] {
  def x: T
  def m[T] = x
}

We have that wrong in Scala 2, I created scala/bug#13144.

val usedMethodTypeParamNames = collection.mutable.Set.empty[Name]
val usedClassTypeParams = collection.mutable.Set.empty[Symbol]

def collect(tp: Type): Unit = tp.dealias match
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, we have

abstract class TypeTraverser(using Context) extends TypeAccumulator[Unit] {
def traverse(tp: Type): Unit
def apply(x: Unit, tp: Type): Unit = traverse(tp)
protected def traverseChildren(tp: Type): Unit = foldOver((), tp)
}
👍

@Gedochao Gedochao added the backport:nominated If we agree to backport this PR, replace this tag with "backport:accepted", otherwise delete it. label Dec 8, 2025
@Gedochao Gedochao added this to the 3.8.0 milestone Dec 8, 2025
Copy link
Member

@lrytz lrytz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM otherwise

else if sym.isTypeParam && sym.isContainedIn(initialSymbol.topLevelClass) then
usedClassTypeParams += sym
else
traverse(pre)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably not be in the else branch?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, we don't need to traverse the pre

Co-authored-by: Lukas Rytz <lukas.rytz@gmail.com>
@tanishiking
Copy link
Member Author

tanishiking commented Dec 17, 2025

Thanks @lrytz ! Could you take an another look? (Added a change based on your suggestion)

@tanishiking tanishiking requested a review from lrytz December 17, 2025 09:17
Copy link
Member

@lrytz lrytz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to be annoying, so feel free to merge this, LGTM.

)

private def isTypeParameterInMethSig(sym: Symbol, initialSymbol: Symbol)(using Context) =
!sym.maybeOwner.isTypeParam &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition I don't understand, is it necessary?

private def isTypeParameterInMethSig(sym: Symbol, initialSymbol: Symbol)(using Context) =
!sym.maybeOwner.isTypeParam &&
sym.isTypeParam && (
(initialSymbol.is(Method) && initialSymbol.typeParams.contains(sym))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialSymbol is always a method.. could this be sym.owner == initialSymbol?

(initialSymbol.is(Method) && initialSymbol.typeParams.contains(sym))
)

private def isTypeParameterInMethSig(sym: Symbol, initialSymbol: Symbol)(using Context) =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably nest this method within collectUsedTypeParams

val sym = tp.typeSymbol
if isTypeParameterInMethSig(sym, initialSymbol) then
usedMethodTypeParamNames += sym.name
else if sym.isTypeParam && sym.isContainedIn(initialSymbol.topLevelClass) then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would sym.isTypeParam && sym.owner == initialSymbol.topLevelClass also be correct?

Comment on lines +560 to +561
if isTypeParameterInMethSig(sym, initialSymbol) then
usedMethodTypeParamNames += sym.name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we even need this case, aren't all references to the method's type params handled above?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:nominated If we agree to backport this PR, replace this tag with "backport:accepted", otherwise delete it.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants